/*
 * Copyright (C) 2012 David Geary. This code is from the book
 * Core HTML5 Canvas, published by Prentice-Hall in 2012.
 *
 * License:
 *
 * Permission is hereby granted, free of charge, to any person 
 * obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge,
 * publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * The Software may not be used to create training material of any sort,
 * including courses, books, instructional videos, presentations, etc.
 * without the express written consent of David Geary.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
*/

var canvas = document.getElementById('canvas'),
    context = canvas.getContext('2d'),
    eraseAllButton = document.getElementById('eraseAllButton'),
    strokeStyleSelect = document.getElementById('strokeStyleSelect'),
    startAngleSelect = document.getElementById('startAngleSelect'),

    fillStyleSelect = document.getElementById('fillStyleSelect'),
    fillCheckbox = document.getElementById('fillCheckbox'),
    editCheckbox = document.getElementById('editCheckbox'),

    sidesSelect = document.getElementById('sidesSelect'),

    CENTROID_RADIUS = 10,
    CENTROID_STROKE_STYLE = 'rgba(0, 0, 0, 0.8)',
    CENTROID_FILL_STYLE ='rgba(255, 255, 255, 0.2)',
    CENTROID_SHADOW_COLOR = 'rgba(255, 255, 255, 0.4)',

    DEGREE_RING_MARGIN = 35,
    TRACKING_RING_MARGIN = 55,
    DEGREE_ANNOTATIONS_FILL_STYLE = 'rgba(0, 0, 230, 0.8)',
    DEGREE_ANNOTATIONS_TEXT_SIZE = 11,
    DEGREE_OUTER_RING_MARGIN = DEGREE_RING_MARGIN,
    TICK_WIDTH = 10,
    TICK_LONG_STROKE_STYLE = 'rgba(100, 140, 230, 0.9)',
    TICK_SHORT_STROKE_STYLE = 'rgba(100, 140, 230, 0.7)',

    TRACKING_RING_STROKING_STYLE = 'rgba(100, 140, 230, 0.3)',
    drawingSurfaceImageData,
   
    mousedown = {},
    rubberbandRect = {},

    dragging = false,
    draggingOffsetX,
    draggingOffsetY,

    sides = 8,
    startAngle = 0,

    guidewires = true,

    editing = false,
    rotatingLockEngaged = false,
    rotatingLockAngle,
    polygonRotating,

    polygons = [];

// Functions.....................................................

function drawGrid(color, stepx, stepy) {
   context.save()

   context.shadowColor = undefined;
   context.shadowBlur = 0;
   context.shadowOffsetX = 0;
   context.shadowOffsetY = 0;
   
   context.strokeStyle = color;
   context.fillStyle = '#ffffff';
   context.lineWidth = 0.5;
   context.fillRect(0, 0, context.canvas.width, context.canvas.height);

   for (var i = stepx + 0.5; i < context.canvas.width; i += stepx) {
     context.beginPath();
     context.moveTo(i, 0);
     context.lineTo(i, context.canvas.height);
     context.stroke();
   }

   for (var i = stepy + 0.5; i < context.canvas.height; i += stepy) {
     context.beginPath();
     context.moveTo(0, i);
     context.lineTo(context.canvas.width, i);
     context.stroke();
   }

   context.restore();
}

function windowToCanvas(e) {
   var x = e.x || e.clientX,
       y = e.y || e.clientY,
       bbox = canvas.getBoundingClientRect();

   return { x: x - bbox.left * (canvas.width  / bbox.width),
            y: y - bbox.top  * (canvas.height / bbox.height)
          };
}

// Save and restore drawing surface..............................

function saveDrawingSurface() {
   drawingSurfaceImageData = context.getImageData(0, 0,
                             canvas.width,
                             canvas.height);
}

function restoreDrawingSurface() {
   context.putImageData(drawingSurfaceImageData, 0, 0);
}


// Rubberbands...................................................

function updateRubberbandRectangle(loc) {
   rubberbandRect.width = Math.abs(loc.x - mousedown.x);
   rubberbandRect.height = Math.abs(loc.y - mousedown.y);

   if (loc.x > mousedown.x) rubberbandRect.left = mousedown.x;
   else                     rubberbandRect.left = loc.x;

   if (loc.y > mousedown.y) rubberbandRect.top = mousedown.y;
   else                     rubberbandRect.top = loc.y;
} 

function drawRubberbandShape(loc, sides, startAngle) {
   var polygon = new Polygon(mousedown.x, mousedown.y,
                     rubberbandRect.width, 
                     parseInt(sidesSelect.value),
                     (Math.PI / 180) * parseInt(startAngleSelect.value),
                     context.strokeStyle,
                     context.fillStyle,
                     fillCheckbox.checked);
   drawPolygon(polygon);
   
   if (!dragging) {
      polygons.push(polygon);
   }
}

function updateRubberband(loc, sides, startAngle) {
   updateRubberbandRectangle(loc);
   drawRubberbandShape(loc, sides, startAngle);
}

// Guidewires....................................................

function drawHorizontalLine (y) {
   context.beginPath();
   context.moveTo(0,y+0.5);
   context.lineTo(context.canvas.width,y+0.5);
   context.stroke();
}

function drawVerticalLine (x) {
   context.beginPath();
   context.moveTo(x+0.5,0);
   context.lineTo(x+0.5,context.canvas.height);
   context.stroke();
}

function drawGuidewires(x, y) {
   context.save();
   context.strokeStyle = 'rgba(0,0,230,0.4)';
   context.lineWidth = 0.5;
   drawVerticalLine(x);
   drawHorizontalLine(y);
   context.restore();
}

// Drawing functions.............................................

function drawPolygons() {
   polygons.forEach( function (polygon) {
      polygon.stroke(context);
      if (polygon.filled) {
         polygon.fill(context);
      }
   });
}

function drawCentroid(polygon) {
   context.beginPath();
   context.save();
   context.strokeStyle = CENTROID_STROKE_STYLE;
   context.fillStyle = CENTROID_FILL_STYLE;
   context.shadowColor = CENTROID_SHADOW_COLOR;
   context.arc(polygon.x, polygon.y, CENTROID_RADIUS, 0, Math.PI*2, false);
   context.stroke();
   context.fill();
   context.restore();
}

function drawCentroidGuidewire(loc, polygon) {
   var angle = Math.atan( (loc.y - polygon.y) / (loc.x - polygon.x) ),
       radius, endpt;

  radius = polygon.radius + TRACKING_RING_MARGIN;
  angle = angle - rotatingLockAngle;

  if (loc.x >= polygon.x) {
      endpt = { x: polygon.x + radius * Math.cos(angle),
                y: polygon.y + radius * Math.sin(angle)
      };
   }
   else {
      endpt = { x: polygon.x - radius * Math.cos(angle),
                y: polygon.y - radius * Math.sin(angle)
      };
   }
   
   context.save();
   context.beginPath();
   context.moveTo(polygon.x, polygon.y);
   context.lineTo(endpt.x, endpt.y);
   context.stroke();

   context.beginPath();
   context.arc(endpt.x, endpt.y, 5, 0, Math.PI*2, false);
   context.stroke();
   context.fill();

   context.restore();
}

function drawDegreeOuterDial(polygon) {
   context.strokeStyle = 'rgba(0, 0, 0, 0.1)';
   context.arc(polygon.x, polygon.y,
               polygon.radius + DEGREE_OUTER_RING_MARGIN,
               0, Math.PI*2, true);
}

function drawDegreeAnnotations(polygon) {
   var radius = polygon.radius + DEGREE_RING_MARGIN;

   context.save();
   context.fillStyle = DEGREE_ANNOTATIONS_FILL_STYLE;
   context.font = DEGREE_ANNOTATIONS_TEXT_SIZE + 'px Helvetica'; 
   
   for (var angle=0; angle < 2*Math.PI; angle += Math.PI/8) {
      context.beginPath();
      context.fillText((angle * 180 / Math.PI).toFixed(0),
         polygon.x + Math.cos(angle) * (radius - TICK_WIDTH*2),
         polygon.y + Math.sin(angle) * (radius - TICK_WIDTH*2));
   }
   context.restore();
}
   
function drawDegreeDialTicks(polygon) {
   var radius = polygon.radius + DEGREE_RING_MARGIN,
       ANGLE_MAX = 2*Math.PI,
       ANGLE_DELTA = Math.PI/64;

   context.save();
   
   for (var angle = 0, cnt = 0; angle < ANGLE_MAX; angle += ANGLE_DELTA, ++cnt) {
      context.beginPath();

      if (cnt % 4 === 0) {
         context.moveTo(polygon.x + Math.cos(angle) * (radius - TICK_WIDTH),
                        polygon.y + Math.sin(angle) * (radius - TICK_WIDTH));
         context.lineTo(polygon.x + Math.cos(angle) * (radius),
                        polygon.y + Math.sin(angle) * (radius));
         context.strokeStyle = TICK_LONG_STROKE_STYLE;
         context.stroke();
      }
      else {
         context.moveTo(polygon.x + Math.cos(angle) * (radius - TICK_WIDTH/2),
                        polygon.y + Math.sin(angle) * (radius - TICK_WIDTH/2));
         context.lineTo(polygon.x + Math.cos(angle) * (radius),
                        polygon.y + Math.sin(angle) * (radius));
         context.strokeStyle = TICK_SHORT_STROKE_STYLE;
         context.stroke();
      }

      context.restore();
   }
}

function drawDegreeTickDial(polygon) {
   context.save();
   context.strokeStyle = 'rgba(0, 0, 0, 0.1)';
   context.beginPath();
   context.arc(polygon.x, polygon.y, polygon.radius + DEGREE_RING_MARGIN - TICK_WIDTH, 0, Math.PI*2, false);
   context.stroke();
   context.restore();
}

function drawTrackingDial(polygon) {
   context.save();
   context.shadowColor = 'rgba(0, 0, 0, 0.7)';
   context.shadowOffsetX = 3,
   context.shadowOffsetY = 3,
   context.shadowBlur = 6,
   context.strokeStyle = TRACKING_RING_STROKING_STYLE;
   context.beginPath();
   context.arc(polygon.x, polygon.y, polygon.radius +
               TRACKING_RING_MARGIN, 0, Math.PI*2, false);
   context.stroke();
   context.restore();
}

function drawRotationAnnotations(loc) {
   drawCentroid(polygonRotating);
   drawCentroidGuidewire(loc, polygonRotating);

   drawTrackingDial(polygonRotating);
   drawDegreeOuterDial(polygonRotating);
   context.fillStyle = 'rgba(100, 140, 230, 0.1)';
   context.fill();

   context.beginPath();
   drawDegreeOuterDial(polygonRotating);
   context.stroke();

   drawDegreeDialTicks(polygonRotating);
   drawDegreeTickDial(polygonRotating);
   drawDegreeAnnotations(polygonRotating);
}

function redraw() {
   context.clearRect(0, 0, canvas.width, canvas.height);
   drawGrid('lightgray', 10, 10);
   drawPolygons();
}

// Polygons......................................................

function drawPolygon(polygon, angle) {
   var tx = polygon.x,
       ty = polygon.y;

   context.save();

   context.translate(tx, ty);

   if (angle) {
      context.rotate(angle);
   }

   polygon.x = 0;
   polygon.y = 0;
   
   polygon.createPath(context);
   context.stroke();

   if (fillCheckbox.checked) {
      context.fill();
   }

   context.restore();

   polygon.x = tx;
   polygon.y = ty;
}

function getSelectedPolygon(loc) {
   for (var i=0; i < polygons.length; ++i) {
      var polygon = polygons[i];

      polygon.createPath(context);
      if (context.isPointInPath(loc.x, loc.y)) {
         startDragging(loc);
         draggingOffsetX = loc.x - polygon.x;
         draggingOffsetY = loc.y - polygon.y;
         return polygon;
      }
   }
   return undefined;
}

function stopRotatingPolygon(loc) {
   angle = Math.atan((loc.y - polygonRotating.y) /
                     (loc.x - polygonRotating.x))
                     - rotatingLockAngle;

   polygonRotating.startAngle += angle;

   polygonRotating = undefined;
   rotatingLockEngaged = false;
   rotatingLockAngle = 0;
}

function startDragging(loc) {
  saveDrawingSurface();
  mousedown.x = loc.x;
  mousedown.y = loc.y;
}

// Event handlers................................................

canvas.onmousedown = function (e) {
   var loc = windowToCanvas(e),
       angle,
       radius,
       trackingRadius;

   e.preventDefault(); // prevent cursor change

   if (editing) {
      if (polygonRotating) {
         stopRotatingPolygon(loc);
         redraw();
      }

      polygonRotating = getSelectedPolygon(loc);

      if (polygonRotating) {
         drawRotationAnnotations(loc);

         if (!rotatingLockEngaged) {
            rotatingLockEngaged = true;
            rotatingLockAngle = Math.atan((loc.y - polygonRotating.y) /
                                          (loc.x - polygonRotating.x));
         }
      }
   }
   else {
      startDragging(loc);
      dragging = true;
   }
};

canvas.onmousemove = function (e) {
   var loc = windowToCanvas(e),
       radius = Math.sqrt(Math.pow(loc.x - dragging.x, 2) +
                          Math.pow(loc.y - dragging.y, 2)),
       angle;


   e.preventDefault(); // prevent selections

   if (rotatingLockEngaged) {
      angle = Math.atan((loc.y - polygonRotating.y) /
                        (loc.x - polygonRotating.x))
                        - rotatingLockAngle;

      redraw();

      drawPolygon(polygonRotating, angle);
      drawRotationAnnotations(loc);
   }
   else if (dragging) {
      restoreDrawingSurface();
      updateRubberband(loc, sides, startAngle);

      if (guidewires) {
         drawGuidewires(mousedown.x, mousedown.y);
      }
   }
};

canvas.onmouseup = function (e) {
   var loc = windowToCanvas(e);

   dragging = false;

   if (!editing) {
      restoreDrawingSurface();
      updateRubberband(loc);
   }
};

eraseAllButton.onclick = function (e) {
   context.clearRect(0, 0, canvas.width, canvas.height);
   drawGrid('lightgray', 10, 10);
   saveDrawingSurface(); 
};

strokeStyleSelect.onchange = function (e) {
   context.strokeStyle = strokeStyleSelect.value;
};

fillStyleSelect.onchange = function (e) {
   context.fillStyle = fillStyleSelect.value;
};

function startEditing() {
   canvas.style.cursor = 'pointer';
   editing = true;
}

function stopEditing() {
   canvas.style.cursor = 'crosshair';
   editing = false;
   polygonRotating = undefined;
   rotatingLockEngaged = false;
   rotatingLockAngle = 0;
   context.clearRect(0, 0, canvas.width, canvas.height);
   drawGrid('lightgray', 10, 10);
   drawPolygons();
}

editCheckbox.onchange = function (e) {
   if (editCheckbox.checked) {
      startEditing();
   }
   else {
      stopEditing();
   }  
};

// Initialization................................................

context.strokeStyle = strokeStyleSelect.value;
context.fillStyle = fillStyleSelect.value;

drawGrid('lightgray', 10, 10);

if (navigator.userAgent.indexOf('Opera') === -1)
   context.shadowColor = 'rgba(0, 0, 0, 0.4)';

context.shadowOffsetX = 2;
context.shadowOffsetY = 2;
context.shadowBlur = 4;

context.textAlign = 'center';
context.textBaseline = 'middle';

